﻿using System;
using System.Collections.Generic;
using System.Text;
using GDA.Analysis;
using System.Data;
using MySql.Data.MySqlClient;
using System.Text.RegularExpressions;

namespace GDA.Provider.MySql
{
    public class MySqlAnalyzer : DatabaseAnalyzer
    {
        #region Constantes

        /// <summary>
        /// Consulta SQL para recupera a relação das tabelas do MySql.
        /// http://www.mysql.com/documentation/mysql/bychapter/manual_SQL_Syntax.html#SHOW 
        /// </summary>
		private const string selectTables = "show tables";

        /// <summary>
        /// Consulta usada para recuperar as informações das colunas da tabela.
        /// Recupera: Field, Type, Null, Key, Default, Extra
        /// </summary>
		//private const string selectColumns = "show columns from `{0}`";
        private const string selectColumns = @"SELECT Column_Name AS Field, Column_Type AS Type, 
                                                Is_Nullable, Column_Key, Column_Default, Extra, Column_Comment 
                                                FROM Information_Schema.Columns 
                                                WHERE Table_Schema='{0}' AND Table_Name='{1}';";

	    /// <summary>
	    /// Consulta para recupera as informações das constraints da tabela do Mysql.
        /// columns: Name, Type, Row_format, Rows, Avg_row_length, Data_length, Max_data_length,
        ///   Index_length, Data_free, Auto_increment, Create_time, Update_time, Check_time, Create_options,
        ///   Comment (as "InnoDB free: 3072 kB; (ListId) REFER test/List(ListId)"	
	    /// </summary>
		private const string selectConstraints = "show create table `{0}`";

        #endregion

        #region Construtores

        public MySqlAnalyzer(MySqlProviderConfiguration provider)
            : base(provider)
		{
        }

        #endregion

        /// <summary>
        /// Efetua a analise do banco de dados.
        /// </summary>
        /// <param name="tableName"></param>
		public override void Analyze( string tableName )
		{
            // Verifica se será recupera apenas as informações de uma tabela
            bool isSingleRun = tableName != null;

            // Cria uma conexão
            IDbConnection conn = ProviderConfiguration.CreateConnection();
            // Cria uma instância para executar a consulta
            IDbCommand cmd = conn.CreateCommand();
            // Relaciona a conexão com o comando
            cmd.Connection = conn;
            // Define o consulta SQL para recupera os dados das tabelas
            cmd.CommandText = selectTables;
            // Cria um data adapter para recupera os dados do resultado
            IDbDataAdapter da = ProviderConfiguration.Provider.CreateDataAdapter();
            da.SelectCommand = cmd;

            // Define um DataSet para salva o resultado
            DataSet ds = new DataSet();

            if (conn.State != ConnectionState.Open)
                // Abre a conexão
                conn.Open();

			try
			{
                try
                {
                    // Recupera os dados e salva no DataSet
                    da.Fill(ds);
                }
                finally
                {
                    conn.Close();
                }

                // Recupera o DataTable que armazena o resultado
                DataTable dt = ds.Tables[0];

                // Pecorre as tabelas encontradas
                for (int i = 0; i < dt.Rows.Count; i++)
				{
                    // Recupera o nome da tabela
                    string dbTableName = dt.Rows[i][0].ToString();

                    // Verifica se não é para carregar apenas os dados de uma tabela
                    // e se a atual tabela que está no loop é a tabela que está sendo procurada
			        if( ! isSingleRun || tableName.ToLower().Equals( dbTableName.ToLower() ) )
			        {
                        // Recupera o mapeamento da tabela
				        TableMap map = GetTableMap( dbTableName );
                        // Verifica se o mapeamento da tabela ainda não foi recuperado
				        if( map == null )
				        {
                            // Instancia um novo mapeamento de tabela
					        map = new TableMap( ProviderConfiguration, dbTableName );
                            // Salva os dados na hashtable
					        tablesMaps[ dbTableName.ToLower() ] = map;
				        }
                        // Recupera as informações das colunas da tabela
				        GetColumnData( map );
				        // Recupera as informações das chaves estrangeiras
				        GetConstraintData( map );
				        // Caso deva ser recupera apenas as informações de uma tabela
				        if( isSingleRun )
                            // Finaliza o processo
					        break;
			        }
			    }
			}
			catch( Exception ex )
			{
				throw new GDAException("An error occurred while analyzing the database schema.", ex);
			}
		}

        /// <summary>
        /// Recupera os dados das colunas da tabela.
        /// </summary>
        /// <param name="map">Tabela</param>
		private void GetColumnData( TableMap map )
		{
            // Cria uma conexão
            IDbConnection conn = ProviderConfiguration.CreateConnection();
            // Cria uma instância para executar a consulta
            IDbCommand cmd = conn.CreateCommand();
            // Relaciona a conexão com o comando
            cmd.Connection = conn;

            // Monta a consulta para recuperar os dados das colunas
			string sql = String.Format( selectColumns, conn.Database, map.TableName);

            // Define o consulta SQL para recupera os dados das tabelas
            cmd.CommandText = sql;

            if (conn.State != ConnectionState.Open)
                // Abre a conexão
                conn.Open();

            try
            {
                // Executa a consulta
                IDataReader dr = cmd.ExecuteReader();

                // Enquanto houver resultados
                while (dr.Read())
                {
                    // returns columns: Field, Type, Null, Key, Default, Extra
                    // Recupera o nome da coluna
                    string columnName = dr["Field"].ToString();

                    // Recupera a coluna já mapeada
                    FieldMap fm = map.GetFieldMapFromColumn(columnName);
                    // Verifica se a coluna já foi mapeada
                    if (fm == null)
                    {
                        // Instancia uma nova coluna
                        fm = new FieldMap(map, columnName);
                        map.Fields.Add(fm);
                    }

                    // Recupera informações sobre o tipo da coluna
                    string typeinfo = dr["Type"].ToString();
                    // Armazena se o tipo é unsigned
                    bool isUnsigned;

                    // Define o tipo do campo
                    fm.SetDbType(ExtractType(typeinfo, out isUnsigned), isUnsigned);

                    /*if (fm.DbType == (long)MySqlDbType.Enum)
                    {
                        fm.HandleEnumAsString = true;
                    }*/

                    // Define o tamanho da coluna
                    fm.Size = ExtractSize(typeinfo);


                    // Verifica se a coluna aceita valores nulos
                    fm.IsNullable = (dr["Is_Nullable"].ToString() == "YES");
                    // Verifica se a coluna é uma chave primária
                    fm.IsPrimaryKey = (dr["Column_Key"].ToString() == "PRI");

                    // Verifica se a coluna é uma chave primária
                    if (fm.IsPrimaryKey)
                        // Verifica se a coluna é autoincremental
                        fm.IsAutoGenerated = (dr["Extra"].ToString() == "auto_increment");

                    fm.Comment = dr.GetString(dr.GetOrdinal("Column_Comment"));
                }
            }
            finally
            {
                conn.Close();
            }
		}

        /// <summary>
        /// Recupera os dados das contraints da tabela.
        /// </summary>
        /// <param name="map"></param>
		private void GetConstraintData( TableMap map )
		{
            // Cria uma conexão
            IDbConnection conn = ProviderConfiguration.CreateConnection();
            // Cria uma instância para executar a consulta
            IDbCommand cmd = conn.CreateCommand();
            // Relaciona a conexão com o comando
            cmd.Connection = conn;
            // Define o consulta SQL para recupera os dados
            cmd.CommandText = String.Format( selectConstraints, map.TableName );

            if (conn.State != ConnectionState.Open)
                // Abre a conexão
                conn.Open();

            try
            {
                 // Executa a consulta
                IDataReader dr = cmd.ExecuteReader();

                // Se houver resultado
                if (dr.Read())
                {
                    // returns columns: Name, Type, Row_format, Rows, Avg_row_length, Data_length, Max_data_length,
                    //   Index_length, Data_free, Auto_increment, Create_time, Update_time, Check_time, Create_options,
                    //   Comment (as "InnoDB free: 3072 kB; (ListId) REFER test/List(ListId)"	
	                // A primeira coluna do resulta pode ser um "create table" ou "create view"
                    string comment = dr.GetFieldType(1) == typeof(byte[]) ? Encoding.Default.GetString((byte[])dr[1]) : dr.GetString(1);

                    // Verifica se recupera o comentário
                    if (comment != null && comment.Length > 5)
                    {
                        // Quebra em partes
                        string[] comments = comment.Split(',');
                        foreach (string cmt in comments)
                        {
                            string tmp = cmt.Trim();
                            // samples:
                            // "(Column) REFER database/Table(Column)"
                            // "(`Column`) REFER `database/Table`(`Column`)"
                            string pattern = @"\(`?(?<column>\w+)`?\) REFER .*/(?<fkTable>\w+)[`\s]+\(`?(?<fkColumn>\w+)`?\)";
                            Regex regex = new Regex(pattern, RegexOptions.ExplicitCapture | RegexOptions.Compiled);
                            Match m = regex.Match(tmp);
                            if (m.Success)
                            {
                                FieldMap fm = map.GetFieldMapFromColumn(m.Groups["column"].Value);
                                if (fm != null)
                                {
                                    fm.ForeignKeyTableName = m.Groups["fkTable"].Value;
                                    fm.ForeignKeyColumnName = m.Groups["fkColumn"].Value;
                                }
                            }
                            else
                            {
                                //CONSTRAINT `fk_employee_type_employee` FOREIGN KEY (`employee_type_id`) REFERENCES `employee_type` (`employee_type_id`) ON DELETE CASCADE ON UPDATE CASCADE
                                //@"[\s\w]FOREIGN KEY\s\(`?(?<column>\w+)`?\) REFERENCES `?(?<fkTable>\w+)`? \(`?(?<fkColumn>\w+)`?\)[\s\w]+";
                                pattern = @"[\s\w]FOREIGN KEY\s\(`?(?<column>\w+)`?\) REFERENCES `?(?<fkTable>\w+)`? \(`?(?<fkColumn>\w+)`?\)";
                                Regex regexNew = new Regex(pattern, RegexOptions.ExplicitCapture | RegexOptions.Compiled);
                                Match mNew = regexNew.Match(tmp);
                                if (mNew.Success)
                                {
                                    // string constraintPart = mNew.Groups["constraint"].Value;
                                    FieldMap fm = map.GetFieldMapFromColumn(mNew.Groups["column"].Value);
                                    if (fm != null)
                                    {
                                        fm.ForeignKeyTableName  = mNew.Groups["fkTable"].Value;
                                        fm.ForeignKeyColumnName = mNew.Groups["fkColumn"].Value;
                                    }
                                }
                                else if (tmp != null)
                                {
                                    int index = tmp.IndexOf("REFER");
                                    if (index > 0)
                                    {
                                        string columnName = ExtractColumn(tmp.Substring(0, index - 1));
                                        tmp = tmp.Substring(index + 5, tmp.Length - index - 5).Trim();
                                        index = tmp.IndexOf("/");
                                        int start = tmp.IndexOf("(");
                                        int end = tmp.IndexOf(")");
                                        if (index > 0 && start > 0 && end > start)
                                        {
                                            string foreignTable = tmp.Substring(index + 1, start - index - 1);
                                            string foreignColumn = tmp.Substring(start + 1, end - start - 1);
                                            FieldMap fm = map.GetFieldMapFromColumn(columnName);
                                            fm.ForeignKeyTableName = foreignTable;
                                            fm.ForeignKeyColumnName = foreignColumn;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            finally
            {
                conn.Close();
            }

		}

        /// <summary>
        /// Extraí os dados da coluna fornecidor pelo banco.
        /// </summary>
        /// <param name="columnInfo">Informações fornecidas pelo banco.</param>
        /// <returns></returns>
		private static string ExtractColumn( string columnInfo )
		{
			string tmp = columnInfo.Trim();
			if( tmp.StartsWith( "(" ) )
				tmp = tmp.Substring( 1, tmp.Length - 1 );
			if( tmp.EndsWith( ")" ) )
				tmp = tmp.Substring( 0, tmp.Length - 1 );
			return tmp;
		}

        /// <summary>
        /// Extrai da informação do tipo fornecida pelo banco de dadaos
        /// as informações que serão usadas no sistema.
        /// </summary>
        /// <param name="typeInfo">Informações do tipo fornecidas pelo banco de dados.</param>
        /// <param name="isUnsigned">Parametro para identificar se o tipo é unsigned.</param>
        /// <returns>Nome do tipo do sistema</returns>
		private static string ExtractType( string typeInfo, out bool isUnsigned )
		{
			// Determina determine if field is unsigned (text "unsigned" after size)
			int sp = typeInfo.IndexOf( " " );
			isUnsigned = sp > 0 && typeInfo.IndexOf( "unsigned", sp, typeInfo.Length - sp ) > 0;
			// get type text
            // Recupera a posição final onde termina a descrição do nome da coluna
			int pos = typeInfo.IndexOf( "(" );
			if( pos != -1 )
                // Recupera o nome da coluna
				return typeInfo.Substring( 0, pos );
			else // return type string without "unsigned" even if there was no size field
				return isUnsigned ? typeInfo.Substring( 0, sp ) : typeInfo;
		}

        /// <summary>
        /// Recupera o tamanho da coluna com base nas informações do tipo
        /// fornecidas pelo banco de dados.
        /// </summary>
        /// <param name="typeInfo">Informações do tipo fornecidas pelo banco de dados.</param>
        /// <returns></returns>
		private static int ExtractSize( string typeInfo )
		{
            // Recupera a posição onde começa as informações sobre o tamanho da coluna.
			int pos = typeInfo.IndexOf( "(" );
            // Verifica se foi encontrada a posição
			if( pos != -1 )
			{
                // Recupera a posição que termina a informação do tamanho da coluna
				int pos2 = typeInfo.IndexOf( ")" );
                // Recupera o texto do tamanho da coluna
				string size = typeInfo.Substring( pos + 1, pos2 - pos - 1 );
                // Verifica se texto do tamanho está com dados padronizados
				if( size.IndexOfAny( new char[] { '\'', '"' } ) != -1 )
					return 0; 
                // Caso o tamanho da coluna seja composto (exemplo 10,3) e recupera
                // apenas o primeiro tamanho
				pos = size.IndexOf( "," );
				if( pos != -1 )
					return Convert.ToInt32( size.Substring( 0, pos ) );
				else
					return Convert.ToInt32( size );
			}
			else
				return 0;
		}
    }
}
